// 
//  Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 



using System;
using System.Collections.Generic;
using System.Threading;

using Galaxium.Protocol.Xmpp.Library.Core;
using Galaxium.Protocol.Xmpp.Library.Utility;
using Galaxium.Protocol.Xmpp.Library.Xml;

using Anculus.Core;

// TODO: Thread safety

namespace Galaxium.Protocol.Xmpp.Library.Messaging
{	
	public enum ChatState { Unknown, Active, Composing, Paused, Inactive, Gone }
	
	public class MessageEventArgs: EventArgs
	{
		public XmlMessage Message { get; private set; }
		public MessageEventArgs (XmlMessage message) { Message = message; }
	}
	
	public class ConvEventArgs: EventArgs
	{
		public Conversation Conversation { get; private set; }
		public ConvEventArgs (Conversation conversation) { Conversation = conversation; }
	}
	
	public class Conversation
	{
		public static event EventHandler<MessageEventArgs> MessageReceived;
		public static event EventHandler<MessageEventArgs> MessageSent;
		public static event EventHandler ChatActivated;
		public event EventHandler StateChanged;
		public event EventHandler MessageAvailable;
		public event EventHandler ResourceChanged;
		public event EventHandler ContactInformationChanged;
		public event EventHandler Closed;
		
		private static byte s_thread_timeout = 20;
		// ^after how many minutes to consider thread dead
		private static byte s_session_timeout = 5;
		// after how many minutes to drop whitelisting
		
		private Contact _item;
		private string _thread;
		private JabberID _jid;
		private Timer _thread_timer;
		private Timer _session_timer;
		
		private int _seq = 0;

		private bool _active;
		
		public bool SupportsChatStates { get; private set; }
		public ChatState ContactState { get; private set; }
		public ChatState CurrentState { get; private set; }

		private Queue<XmlMessage> _message_queue =
			new Queue<XmlMessage> ();

		#region properties
		
		public Contact Contact {
			get { return _item; }
		}
		public JabberID Jid {
			get { return _jid;	}
		}
		
		public string CurrentResource {
			get { return _jid.Resource; }
			set {
				if (_jid.Resource == value) return;
				
				var current = CurrentState;
				if (CurrentState != ChatState.Unknown) {
					ChangeState (ChatState.Active);
					CurrentState = ChatState.Unknown;
				}
				_jid = new JabberID (_jid.Node, _jid.Domain, value);
				
				var resource = value == null ? null : _item.GetResource (value);
				
				SupportsChatStates = resource == null
					|| resource.Supports (Namespaces.ChatStates)
					|| !resource.Supports (Namespaces.DiscoInfo);
				
				if (SupportsChatStates && current != ChatState.Unknown)
					ChangeState (current);
				
				OnResourceChanged ();
			}
		}

		public string CurrentThread {
			get { return _thread; }
		}

		public bool Active {
			get { return _active; }
			set {
				if (!_active && value)
					OnChatActivated ();
				_active = value;
			}
		}

		public int Queued {
			get { return _message_queue.Count; }
		}
		
		#endregion

		internal Conversation (Contact item)
		{
			_item = item;
			_jid = item.Jid;
			_thread_timer = new Timer (ResetThread, null, -1, -1);
			ResetThread (null);
			SupportsChatStates = true;
		}

		private void ResetThread (object o)
		{
			_thread = Uuid.GenerateRandom ();
			CurrentResource = null;
		}

		private void ResetTimer ()
		{
			_thread_timer.Change (s_thread_timeout * 60 * 1000, -1);
		}
		
		#region Automatic directed presences
		
		private void EngageSession ()
		{
			if (_session_timer == null) {
				_session_timer = new Timer ((o) => EndSession (), null, -1, -1);
				Contact.Client.AddEntitySession (Contact.Jid);
			}
			RenewSession ();
		}
		
		private void RenewSession ()
		{
			if (_session_timer != null)
				_session_timer.Change (s_session_timeout * 60 * 1000, -1);
		}
		
		private void EndSession ()
		{
			if (_session_timer != null) {
				_session_timer.Dispose ();
				_session_timer = null;
				Contact.Client.RemoveEntitySession (Contact.Jid, true);
			}	
		}
		
		#endregion

		#region internal interface
		
		internal void HandlePresence (Presence pres)
		{
			OnContactInformationChanged ();
		}
		
		internal void HandleMessage (ChatMessage msg)
		{
			RenewSession ();
			ResetTimer ();
			
			var state = msg.ChatState;
			if (ContactState != state) {
				SupportsChatStates = state != ChatState.Unknown;
				ContactState = state;
				OnStateChanged ();
			}
			if (state == ChatState.Gone)
				ResetThread (null);
			
			if (String.IsNullOrEmpty (msg.Body)) return;
			
			CurrentResource = msg.From.Resource;
			_thread = msg.Thread;
			
			Enqueue (msg);
			OnMessageReceived (msg);
		}
		
		internal void HandleError (XmlMessage msg)
		{
			CurrentResource = null;	
			SupportsChatStates = false;
			Enqueue (msg);
			OnMessageReceived (msg);
		}
		
		internal void HandleContactRemoving ()
		{
			EndSession ();
			_item = null;
			_jid = null;
			_thread_timer.Dispose ();
			_thread_timer = null;
			_message_queue = null;
			OnClosed ();
		}

		internal void HandleContactChange ()
		{
			OnContactInformationChanged ();
		}
		
		#endregion
		
		public void ChangeState (ChatState state)
		{
			// FIXME: only send if the entity receives a presence
			
			if (state == CurrentState) return;
			CurrentState = state;
			if (!SupportsChatStates) return;
			var msg = new ChatMessage (_jid, _thread, null);
			msg.ID = "c" + (++ _seq);
			msg.AppendChatState (state);
			_item.Client.Send (msg);
			if (state == ChatState.Gone)
				ResetThread (null);
		}

		public void SendMessage (string body)
		{
			if (String.IsNullOrEmpty (body))
				throw new ArgumentException ("Body must not be empty.");
			
			EngageSession ();
			ResetTimer ();
			
			var msg = new ChatMessage (_jid, _thread, body);
			msg.ID = "c" + (++ _seq);
			if (SupportsChatStates)
				msg.AppendChatState (CurrentState = ChatState.Active);

			Enqueue (msg);
			
			_item.Client.Send (msg);
			OnMessageSent (msg);
		}

		private void Enqueue (XmlMessage message)
		{
			if (_message_queue.Count == 0)
				_item.Client.LockConversation (_jid);
			_message_queue.Enqueue (message);
		}
		
		public XmlMessage[] GetQueue ()
		{
			var arr = _message_queue.ToArray ();
			_message_queue.Clear ();
			_item.Client.UnlockConversation (_jid);
			return arr;
		}

		~Conversation ()
		{
			Log.Debug ("Finalizing Conversation for contact " + _item.Jid);
			if (_thread_timer != null)
				_thread_timer.Dispose ();
			_thread_timer = null;
			EndSession ();
		}
		
		#region Event emitors
		
		protected void OnMessageReceived (XmlMessage message)
		{
			if (MessageAvailable != null)
				MessageAvailable (this, EventArgs.Empty);
			if (MessageReceived != null)
				MessageReceived (this, new MessageEventArgs (message));
		}
		
		protected void OnMessageSent (ChatMessage message)
		{
			if (MessageAvailable != null)
				MessageAvailable (this, EventArgs.Empty);
			if (MessageSent != null)
				MessageSent (this, new MessageEventArgs (message));
		}
		
		protected void OnChatActivated ()
		{
			if (ChatActivated != null)
				ChatActivated (this, EventArgs.Empty);
		}
		
		protected void OnStateChanged ()
		{
			if (StateChanged != null)
				StateChanged (this, EventArgs.Empty);
		}
		
		protected void OnResourceChanged ()
		{
			if (ResourceChanged != null)
				ResourceChanged (this, EventArgs.Empty);
		}
		
		protected void OnContactInformationChanged ()
		{
			if (ContactInformationChanged != null)
				ContactInformationChanged (this, EventArgs.Empty);
		}
		
		protected void OnClosed ()
		{
			if (Closed != null)
				Closed (this, EventArgs.Empty);
		}
		
		#endregion
	}
}
